#!/usr/bin/env python
#
# vmnetx-server - Export VMNetX VMs via remote network protocol
#
# Copyright (C) 2012-2013 Carnegie Mellon University
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.  A copy of the GNU General Public License
# should have been distributed along with this program in the file
# COPYING.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#

from dateutil.parser import parse as parse_date
from dateutil.tz import tzlocal
import fcntl
import glib
import json
import logging
from optparse import OptionParser
import os
import requests
from urlparse import urljoin
import signal
import socket
import sys
import yaml

import vmnetx
from vmnetx.server import VMNetXServer

USAGE = 'Usage: %prog [options] config-file'
VERSION = '%prog ' + vmnetx.__version__
DESCRIPTION = 'Export VMNetX VMs via remote network protocol.'

DEFAULT_PORT = 18923
DEFAULT_GC_INTERVAL = 5  # seconds
DEFAULT_INSTANCE_TIMEOUT = 60 * 5  # seconds
DEFAULT_HTTP_HOST = '127.0.0.1'
DEFAULT_HTTP_PORT = 18924


def parse_config(path):
    with open(path, 'r') as stream:
        config = yaml.load(stream)
    options = {}

    options['secret_key'] = config.get('secret_key')
    if options['secret_key'] is None:
        raise ValueError("Missing secret key")

    options['username'] = config.get('username')
    options['password'] = config.get('password')

    options['host'] = config.get('host')
    if options['host'] is None:
        options['host'] = socket.gethostbyname(socket.gethostname())

    options['port'] = config.get('port', DEFAULT_PORT)
    if not isinstance(options['port'], int):
        raise ValueError("Invalid port setting")

    options['http_host'] = config.get('http_host', DEFAULT_HTTP_HOST)
    if not isinstance(options['http_host'], str):
        raise ValueError("Invalid http host")

    options['http_port'] = config.get('http_port', DEFAULT_HTTP_PORT)
    if not isinstance(options['http_port'], int):
        raise ValueError("Invalid http port")

    options['gc_interval'] = config.get('gc_interval', DEFAULT_GC_INTERVAL)
    if not isinstance(options['gc_interval'], int):
        raise ValueError("Invalid GC timeout")

    options['instance_timeout'] = config.get('instance_timeout',
            DEFAULT_INSTANCE_TIMEOUT)
    if not isinstance(options['instance_timeout'], int):
        raise ValueError("Invalid instance timeout")

    return options


# self-pipe trick for shutting down correctly from signals
def shutdown(source, _cond, server):
    try:
        source.read(1)
    except IOError, e:
        if e.errno == errno.EAGAIN:
            return True
        _log.exception("Received unexpected error: shutting down")
    server.shutdown()
    return False


def setup_signals(server):
    fd_rd, fd_wr = os.pipe()
    fcntl.fcntl(fd_rd, fcntl.F_SETFL,
            fcntl.fcntl(fd_rd, fcntl.F_GETFL) | os.O_NONBLOCK)
    fcntl.fcntl(fd_wr, fcntl.F_SETFL,
            fcntl.fcntl(fd_wr, fcntl.F_GETFL) | os.O_NONBLOCK)

    rd = os.fdopen(fd_rd, 'r')
    wr = os.fdopen(fd_wr, 'w', 0)

    glib.io_add_watch(rd, glib.IO_IN, shutdown, server)

    def sig_handler(sig, _frame):
        wr.write('x')
    signal.signal(signal.SIGTERM, sig_handler)
    signal.signal(signal.SIGINT, sig_handler)

    # Ignore SIGPIPE so memory image recompression will get EPIPE if a
    # compressor dies.
    signal.signal(signal.SIGPIPE, signal.SIG_IGN)


default_server = 'http://%s:%s' % (DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT)

parser = OptionParser(usage=USAGE, version=VERSION, description=DESCRIPTION)
parser.add_option('-D', '--debug', dest='debug', action='store_true',
        help='Enable debug messages')
parser.add_option('-c', '--create-instance', dest='create_instance',
        metavar='URL',
        help='URL of the VM for which the instance is created')
parser.add_option('-d', '--destroy-instance', dest='destroy_instance',
        metavar='ID',
        help='ID of instance to destroy')
parser.add_option('-S', '--server', dest='server', metavar='URL',
        default=os.getenv('VMNETX_SERVER') or default_server,
        help=('URL of the server to query (default: $VMNETX_SERVER or %s)'
                % default_server))
parser.add_option('-k', '--secret-key', dest='secret', metavar='KEY',
        default=os.getenv('VMNETX_SECRET_KEY'),
        help='Secret key of the server to query (default: $VMNETX_SECRET_KEY)')
parser.add_option('-s', '--status', dest='status', action='store_true',
        help='Get status of server')
parser.add_option('-u', '--user-ident', dest='user_ident', metavar='NAME',
        help='User identifier to pass to server')

opts, args = parser.parse_args()

if opts.debug:
    loglevel = logging.DEBUG
elif opts.create_instance or opts.destroy_instance or opts.status:
    loglevel = logging.WARNING
else:
    loglevel = logging.INFO
logging.basicConfig(level=loglevel)

if opts.status:
    if opts.secret is None:
        raise ValueError('Getting status requires --secret-key')
    headers = {"X-Secret-Key": opts.secret}
    r = requests.get(urljoin(opts.server, 'instance'), headers=headers)
    if r.status_code != requests.codes.ok:
        sys.stderr.write('Server replied with code %d\n' % r.status_code)
        sys.exit(1)

    data = json.loads(r.text)
    instances = data['instances']

    if instances:
        fmt = ('%(id)-16.16s %(vm_name)-24.24s %(user_ident)-18.18s ' +
                '%(status)-8.8s %(last_seen)-9.9s')
        print fmt % {
            'id': 'Instance ID',
            'vm_name': 'VM Name',
            'user_ident': 'User Identifier',
            'status': 'Status',
            'last_seen': 'Last Seen',
        }
        for instance in instances:
            parsed_time = parse_date(instance['last_seen'])
            local_time = parsed_time.astimezone(tzlocal())
            instance['last_seen'] = local_time.strftime('%H:%M:%S')
            print fmt % instance
    else:
        print 'No instances exist'
elif opts.create_instance:
    if opts.secret is None:
        raise ValueError('Creating instance requires --secret-key')
    payload = {
        "url": opts.create_instance,
        "user_ident": opts.user_ident,
    }
    headers = {"X-Secret-Key": opts.secret}
    r = requests.post(urljoin(opts.server, 'instance'),
            data=json.dumps(payload), headers=headers)
    if r.status_code != requests.codes.ok:
        sys.stderr.write('Server replied with code %d\n' % r.status_code)
        sys.exit(1)
    data = json.loads(r.text)
    print data['url']
elif opts.destroy_instance:
    if opts.secret is None:
        raise ValueError('Destroying instance requires --secret-key')
    headers = {"X-Secret-Key": opts.secret}
    url = urljoin(opts.server, 'instance/%s' % opts.destroy_instance)
    r = requests.delete(url, headers=headers)
    if r.status_code != requests.codes.no_content:
        sys.stderr.write('Server replied with code %d\n' % r.status_code)
        sys.exit(1)
else:
    if len(args) != 1:
        parser.error('Incorrect mandatory argument')
    config_path = args[0]
    options = parse_config(config_path)

    loop = glib.MainLoop()

    server = VMNetXServer(options)
    server.initialize()
    server.connect('shutdown', lambda _server: loop.quit())
    setup_signals(server)

    loop.run()
